﻿'----------------------------------------------------------------------
' Created with SharpDevelop
' Author: Adam Yarnott, Blue Ninja Software
' Copyright (c) Adam Yarnott, Blue Ninja Software.
' Date: 8/21/2007
' Time: 11:21 PM
'----------------------------------------------------------------------

Imports System.Runtime.InteropServices

Imports System.IO
Imports System.Threading.Tasks

Imports BlueNinjaSoftware.HIDLib.Interop
Imports BlueNinjaSoftware.HIDLib.Capabilities
Imports BlueNinjaSoftware.HIDLib.Reports

''' <summary>
''' Represents a single HID device.
''' </summary>
Public Class HIDDevice
	Implements IEquatable(Of HIDDevice)
	Implements IDisposable
	
	#Region "Private Members"
	
	Private _Stream As FileStream
	Private _DevicePath As String
	
	Private _Attributes As HIDD_ATTRIBUTES
	
	Private _PreparsedData As IntPtr
	
	Private _Capabilities As DeviceCapabilities
	
	Private _Manufacturer As String = ""
	Private _Product As String = ""
	Private _Serial As String = ""
    Private _Descriptor As String = ""

    Private _AsyncReading As Boolean = False
    Private _AsyncResult As IAsyncResult
    Private _AsyncBuffer As Byte()
    Private _AsyncTask As Task(Of Integer)

	#End Region
	
	#Region "Events"
	
	Public Event DataReceived As HIDDataReceivedEventHandler
	
	Public Event ReportReceived As HIDReportReceivedEventHandler
	
	Public Event Disconnected As EventHandler
	
	Public Event Reconnected As EventHandler
	
	#End Region
	
	#Region "Constructors"
	
	''' <summary>
	''' Public constructor to explicitly obtain a device by it's DevicePath.
	''' </summary>
	''' <param name="DevicePath"></param>
	Public Sub New(ByVal DevicePath As String)
		Try
			
			' open a read/write handle to our device using the DevicePath returned
			Dim Handle As New Microsoft.Win32.SafeHandles.SafeFileHandle(CreateFile(DevicePath, FileAccess.ReadWrite, FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, EFileAttributes.Overlapped, IntPtr.Zero), True)
			
			If Not Handle.IsInvalid Then
                _Stream = New FileStream(Handle, FileAccess.ReadWrite, 8, True)
				
				_Attributes.Size = Marshal.SizeOf(_Attributes)
	
				If HidD_GetAttributes(_Stream.SafeFileHandle.DangerousGetHandle, _Attributes) Then
					_DevicePath = DevicePath
					DeviceSetup
				Else
					Throw New ApplicationException(String.Format("Could not get attributes for device '{0}': {1}", DevicePath, GetLastWin32Error.Message))
				End If
			Else
				Throw New ApplicationException("Cannot open specified device: " & GetLastWin32Error.Message)
			End If
		Catch ex As ApplicationException
			Throw
		Catch ex As Exception
			Throw New ApplicationException(String.Format("Could not access device '{0}': {1}", DevicePath, ex.Message))
		End Try
	End Sub
	
	''' <summary>
	''' Internal constructor to use existing stream and attributes from HIDDeviceManagement.GetDevice.
	''' </summary>
	''' <param name="Stream"></param>
	''' <param name="Attributes"></param>
	Friend Sub New(ByRef Stream As FileStream, ByVal DevicePAth As String, ByVal Attributes As HIDD_ATTRIBUTES)
		_Stream = Stream
		_Attributes = Attributes
		_DevicePath = DevicePath
		
		DeviceSetup
	End Sub
	
	#End Region
	
	#Region "Public Properties"
	
	''' <summary>
	''' Returns the device's manufacturer-assigned VendorID.
	''' </summary>
	Public ReadOnly Property VendorID() As UShort
		Get
			Return _Attributes.VendorID
		End Get
	End Property
	
	''' <summary>
	''' Returns the device's manufacturer-assigned ProductID.
	''' </summary>
	''' <remarks>Manufacturers are not supposed to assign the same ProductID to multiple products; they are intended to be unique per product.
	''' 	However, not all manufacturers follow this guideline.</remarks>
	Public ReadOnly Property ProductID() As UShort
		Get
			Return _Attributes.ProductID
		End Get
	End Property
	
	''' <summary>
	''' Returns the device's manufacturer name.
	''' </summary>
	Public ReadOnly Property Manufacturer() As String
		Get
			Return _Manufacturer
		End Get
	End Property
	
	''' <summary>
	''' Returns the device's manufacturer-assigned product name.
	''' </summary>
	Public ReadOnly Property ProductName() As String
		Get
			Return _Product
		End Get
	End Property
	
	''' <summary>
	''' Returns the device's manufacturer-assigned serial number.
	''' </summary>
	''' <remarks>No two devices with a given VendorID and ProductID combination should ever have the same serial number; they are intended to be unique.
	''' 	However, not all manufacturers follow this guideline, or assign a serial number at all.</remarks>
	Public ReadOnly Property SerialNumber() As String
		Get
			Return _Serial
		End Get
	End Property
	
	''' <summary>
	''' Returns the device's manufacturer-assigned device descriptor.
	''' </summary>
	Public ReadOnly Property Descriptor() As String
		Get
			Return _Descriptor
		End Get
	End Property
	
	''' <summary>
	''' Returns the device's manufacturer-assigned product or firmware version number.
	''' </summary>
	Public ReadOnly Property VersionNumber As UShort
		Get
			Return _Attributes.VersionNumber
		End Get
	End Property
	
	''' <summary>
	''' Returns the device path of the device.
	''' </summary>
	Public ReadOnly Property DevicePath As String
		Get
			Return _DevicePath
		End Get
	End Property
	
	Public ReadOnly Property Attached As Boolean
		'TODO: This should initially be set in GetDevices, and based on PresentOnly.
		Get
			Return (_Stream IsNot Nothing)
		End Get
	End Property
	
	''' <summary>
	''' Returns the SafeFileHandle used for device communication.
	''' </summary>
	Public ReadOnly Property SafeHandle As Microsoft.Win32.SafeHandles.SafeFileHandle
		Get
			Return _Stream.SafeFileHandle
		End Get
	End Property

    ''' <summary>
    ''' Returns the Async Reading Flag
    ''' </summary>
    Public ReadOnly Property AsyncReading As Boolean
        Get
            Return _AsyncReading
        End Get
    End Property

	#End Region
	
	#Region "Device Capabilities Methods"
	
	''' <summary>
	''' Provides access to the device's capabilities information.
	''' </summary>
	Public ReadOnly Property Capabilities() As DeviceCapabilities
		Get
			Return _Capabilities
		End Get
	End Property
	
	#End Region
	
	#Region "Create Report Methods"
	
	''' <summary>
	''' Returns an empty Output report.
	''' </summary>
	''' <returns></returns>
	Public Function CreateOutputReport() As HIDOutputReport
		Return New HIDOutputReport(_Capabilities.OutputCapabilities.ReportByteLength)
	End Function
	
	''' <summary>
	''' Returns an empty Output report for the specified ReportID.
	''' </summary>
	''' <param name="ReportID"></param>
	''' <returns></returns>
	Public Function CreateOutputReport(ByVal ReportID As Byte) As HIDOutputReport
		Return New HIDOutputReport(_Capabilities.OutputCapabilities.ReportByteLength, ReportID)
	End Function
	
	''' <summary>
	''' Returns an Output report for the specified ReportID with the specified ReportData.
	''' </summary>
	''' <param name="ReportID"></param>
	''' <param name="ReportData"></param>
	''' <returns></returns>
	Public Function CreateOutputReport(ByVal ReportID As Byte, ByVal ReportData As Byte()) As HIDOutputReport
		Return New HIDOutputReport(_Capabilities.OutputCapabilities.ReportByteLength, ReportID, ReportData)
	End Function
	
	''' <summary>
	''' Creates an Output report from the specified raw report buffer.
	''' </summary>
	''' <param name="ReportBuffer"></param>
	''' <returns></returns>
	Public Function CreateOutputReport(ByVal ReportBuffer As Byte()) As HIDOutputReport
		If ReportBuffer.Length = _Capabilities.OutputCapabilities.ReportByteLength Then
			Return New HIDOutputReport(ReportBuffer)
		Else
			Throw New ArgumentException(String.Format("Report length incorrect - was {0}, but must be {1}.", ReportBuffer.Length, _Capabilities.OutputCapabilities.ReportByteLength))
		End If
	End Function
	
	''' <summary>
	''' Initializes a new Output report for the specified ReportID.
	''' The ReportData will contain the current or default settings as specified in the specified report's descriptor.
	''' </summary>
	''' <param name="ReportID"></param>
	''' <returns></returns>
	''' <remarks>Initializing an HID report sets all control data to zero or a control's null value, as defined by the USB HID standard. (Sending or receiving a null value indicates that the current value of a control should not be modified.)
	''' 	<para>InitializeOutputReport does the following:</para>
	''' 	<list type="bullet">
	''' 		<item>Sets to zero the bit fields of all buttons and values without null values.</item>
	''' 		<item>Sets the bit field of all controls with null values to their corresponding null value.</item>
	''' 	</list>
	''' 	<para>InitializeOutputReport runs at IRQL &lt;= DISPATCH_LEVEL.</para>
	''' </remarks>
	Public Function InitializeOutputReport(ByVal ReportID As Byte) As HIDOutputReport
		Dim Rep As New HIDOutputReport(_Capabilities.OutputCapabilities.ReportByteLength, ReportID)
		Dim rc As HID.NTSTATUS = HidP_InitializeReportForID(HID.HIDP_REPORT_TYPE.Output, ReportID, _PreparsedData, Rep.GetBuffer, Rep.ReportLength)
		
		If rc < HID.NTSTATUS.NT_WARNING Then
			Return Rep
		Else
			Throw New ApplicationException("Could not initialize output report: " & rc.ToString)
		End If
	End Function
	
	
	''' <summary>
	''' Returns an empty Feature report.
	''' </summary>
	''' <returns></returns>
	Public Function CreateFeatureReport() As HIDFeatureReport
		Return New HIDFeatureReport(_Capabilities.FeatureCapabilities.ReportByteLength)
	End Function
	
	''' <summary>
	''' Returns an empty Feature report for the specified ReportID.
	''' </summary>
	''' <param name="ReportID"></param>
	''' <returns></returns>
	Public Function CreateFeatureReport(ByVal ReportID As Byte) As HIDFeatureReport
		Return New HIDFeatureReport(_Capabilities.FeatureCapabilities.ReportByteLength, ReportID)
	End Function
	
	''' <summary>
	''' Returns a Feature report for the specified ReportID with the specified ReportData.
	''' </summary>
	''' <param name="ReportID"></param>
	''' <param name="ReportData"></param>
	''' <returns></returns>
	Public Function CreateFeatureReport(ByVal ReportID As Byte, ByVal ReportData As Byte()) As HIDFeatureReport
		Return New HIDFeatureReport(_Capabilities.FeatureCapabilities.ReportByteLength, ReportID, ReportData)
	End Function
	
	''' <summary>
	''' Creates a Feature report from the specified raw report buffer.
	''' </summary>
	''' <param name="ReportBuffer"></param>
	''' <returns></returns>
	Public Function CreateFeatureReport(ByVal ReportBuffer As Byte()) As HIDFeatureReport
		If ReportBuffer.Length = _Capabilities.FeatureCapabilities.ReportByteLength Then
			Return New HIDFeatureReport(ReportBuffer)
		Else
			Throw New ArgumentException(String.Format("Report length incorrect - was {0}, but must be {1}.", ReportBuffer.Length, _Capabilities.FeatureCapabilities.ReportByteLength))
		End If
	End Function
	
	''' <summary>
	''' Initializes a new Feature report for the specified ReportID.
	''' The ReportData will contain the current or default settings as specified in the specified report's descriptor.
	''' </summary>
	''' <param name="ReportID"></param>
	''' <returns></returns>
	''' <remarks>Initializing an HID report sets all control data to zero or a control's null value, as defined by the USB HID standard. (Sending or receiving a null value indicates that the current value of a control should not be modified.)
	''' 	<para>InitializeFeatureReport does the following:</para>
	''' 	<list type="bullet">
	''' 		<item>Sets to zero the bit fields of all buttons and values without null values.</item>
	''' 		<item>Sets the bit field of all controls with null values to their corresponding null value.</item>
	''' 	</list>
	''' 	<para>InitializeFeatureReport runs at IRQL &lt;= DISPATCH_LEVEL.</para>
	''' </remarks>
	Public Function InitializeFeatureReport(ByVal ReportID As Byte) As HIDFeatureReport
		Dim Rep As New HIDFeatureReport(_Capabilities.FeatureCapabilities.ReportByteLength, ReportID)
		Dim rc As HID.NTSTATUS = HidP_InitializeReportForID(HID.HIDP_REPORT_TYPE.Feature, ReportID, _PreparsedData, Rep.GetBuffer, Rep.ReportLength)
		
		If rc < HID.NTSTATUS.NT_WARNING Then
			Return Rep
		Else
			Throw New ApplicationException("Could not initialize feature report: " & rc.ToString)
		End If
	End Function
	
#End Region

    Public Sub BeginAsyncReading()
        _AsyncReading = True
        ReDim _AsyncBuffer(_Capabilities.InputCapabilities.ReportByteLength - 1)
        BeginAsyncRead()
    End Sub

    Public Sub EndAsyncReading()
        _AsyncReading = False
    End Sub

#Region "Report I/O Methods"

    ''' <summary>
    ''' Requests an Input report from the device.
    ''' </summary>
    ''' <returns>TRUE if the report was read successfully, FALSE if there was an error. You can use GetLastWin32Error to get more detailed error information.</returns>
    Public Function ReadInputReport() As HIDInputReport
        Dim InputReport As New HIDInputReport(_Capabilities.InputCapabilities.ReportByteLength)

        If DoReadInputReport(InputReport) Then
            Return InputReport
        End If
    End Function

    ''' <summary>
    ''' Requests an Input report from the device, specifying the ReportID.
    ''' </summary>
    ''' <param name="ReportID"></param>
    ''' <returns>TRUE if the report was read successfully, FALSE if there was an error. You can use GetLastWin32Error to get more detailed error information.</returns>
    Public Function ReadInputReport(ByVal ReportID As Byte) As HIDInputReport
        Dim InputReport As New HIDInputReport(_Capabilities.InputCapabilities.ReportByteLength, ReportID)

        If DoReadInputReport(InputReport) Then
            Return InputReport
        End If
    End Function

    ''' <summary>
    ''' Requests a Feature report from the device.
    ''' </summary>
    ''' <returns>TRUE if the report was read successfully, FALSE if there was an error. You can use GetLastWin32Error to get more detailed error information.</returns>
    Public Function ReadFeatureReport() As HIDFeatureReport
        Dim FeatureReport As New HIDFeatureReport(_Capabilities.FeatureCapabilities.ReportByteLength)

        If DoReadFeatureReport(FeatureReport) Then
            Return FeatureReport
        End If
    End Function

    ''' <summary>
    ''' Requests a Feature report from the device, specifying the ReportID.
    ''' </summary>
    ''' <param name="ReportID"></param>
    ''' <returns>TRUE if the report was read successfully, FALSE if there was an error. You can use GetLastWin32Error to get more detailed error information.</returns>
    Public Function ReadFeatureReport(ByVal ReportID As Byte) As HIDFeatureReport
        Dim FeatureReport As New HIDFeatureReport(_Capabilities.FeatureCapabilities.ReportByteLength, ReportID)

        If DoReadFeatureReport(FeatureReport) Then
            Return FeatureReport
        End If
    End Function

    ''' <summary>
    ''' Writes a report to the device.
    ''' </summary>
    ''' <param name="Report">The report to be written. This must be of type HIDOutputReport or HIDFeatureReport.</param>
    ''' <returns>TRUE if the report was written successfully, FALSE if there was an error. You can use GetLastWin32Error to get more detailed error information.</returns>
    Public Function WriteReport(ByVal Report As AHIDReport) As Boolean
        If TypeOf (Report) Is HIDOutputReport Then
            If Report.ReportLength + 1 = _Capabilities.OutputCapabilities.ReportByteLength Then
                Return HidD_SetOutputReport(_Stream.SafeFileHandle.DangerousGetHandle, Report.GetBuffer, Report.ReportLength + 1)
            Else
                Throw New ArgumentException(String.Format("Report length incorrect - was {0}, but must be {1}.", Report.ReportLength, _Capabilities.OutputCapabilities.ReportByteLength))
            End If
        ElseIf TypeOf (Report) Is HIDFeatureReport Then
            If Report.ReportLength + 1 = _Capabilities.FeatureCapabilities.ReportByteLength Then
                Return HidD_SetFeature(_Stream.SafeFileHandle.DangerousGetHandle, Report.GetBuffer, Report.ReportLength + 1)
            Else
                Throw New ArgumentException(String.Format("Report length incorrect - was {0}, but must be {1}.", Report.ReportLength, _Capabilities.FeatureCapabilities.ReportByteLength))
            End If
        Else
            Throw New ArgumentException("Invalid report type - must be HIDOutputReport or HIDFeatureReport.")
        End If
    End Function

#End Region

#Region "Bulk I/O Methods"

    Public Function BulkRead(ByRef Data As Byte()) As Boolean
        If (Data.Length > _Capabilities.InputCapabilities.ReportByteLength) Then
            Throw New Exception("Error, buffer too long")
        End If
        _Stream.Read(Data, 0, Data.Length)
        Return True
    End Function

    Public Function BulkRead(ByRef Data As String) As Boolean
        Throw New NotImplementedException
    End Function


    Public Function BulkWrite(ByVal Data As Byte()) As Boolean
        Throw New NotImplementedException
    End Function

    Public Function BulkWrite(ByVal Data As String) As Boolean
        Throw New NotImplementedException
    End Function

#End Region

#Region "Other Helper / Utility Methods"

    ''' <summary>
    ''' Returns a Win32Exception containing the last Win32 error code and description.
    ''' </summary>
    ''' <returns></returns>
    Public Function GetLastWin32Error() As System.ComponentModel.Win32Exception
        Return New System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error)
    End Function

#End Region

#Region "Override Methods"

    Public Overloads Overrides Function ToString() As String
        Return String.Format("VID: {0}, PID: {1}, {2} {3} rev. {4}, SN: {5}", PadHex(Hex(_Attributes.VendorID), 4), PadHex(Hex(_Attributes.ProductID), 4), _Manufacturer, _Product, PadHex(Hex(_Attributes.VersionNumber), 4), _Serial)
    End Function

#End Region

#Region "Private Members"

    Private Sub DeviceSetup()
        GetCaps()

        GetParsedStringData()

    End Sub

    ''' <summary>
    ''' Reads the device's capabilities and value capabilities upon device initialization.
    ''' </summary>
    Private Sub GetCaps()
        If HidD_GetPreparsedData(_Stream.SafeFileHandle.DangerousGetHandle, _PreparsedData) Then
            Try
                _Capabilities = New DeviceCapabilities(Me, _PreparsedData)
            Catch ex As ApplicationException
                Throw New ApplicationException(String.Format("Could not get capabilities data for device '{0}': {1}", _Stream.Name, ex.ToString))
            End Try
        Else
            Throw New ApplicationException(String.Format("Could not get preparsed data for device '{0}': {1}", _Stream.Name, New System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error).Message))
        End If
    End Sub

    Private Sub GetParsedStringData()
        Dim Mfg As String = Strings.StrDup(127, Chr(0))
        If HidD_GetManufacturerString(_Stream.SafeFileHandle.DangerousGetHandle, Mfg, Mfg.Length * 2) Then
            _Manufacturer = ParseString(Mfg)
        End If

        Dim Prod As String = Strings.StrDup(127, Chr(0))
        If HidD_GetProductString(_Stream.SafeFileHandle.DangerousGetHandle, Prod, Prod.Length * 2) Then
            _Product = ParseString(Prod)
        End If

        Dim Ser As String = Strings.StrDup(127, Chr(0))
        If HidD_GetSerialNumberString(_Stream.SafeFileHandle.DangerousGetHandle, Ser, Ser.Length * 2) Then
            _Serial = ParseString(Ser)
        End If

        Dim Desc As String = Strings.StrDup(127, Chr(0))
        If HidD_GetPhysicalDescriptor(_Stream.SafeFileHandle.DangerousGetHandle, Desc, Desc.Length * 2) Then
            _Descriptor = ParseString(Desc)
        End If

    End Sub

    ''' <summary>
    ''' Performs GetInputReport for the overloaded ReadInputReport methods.
    ''' </summary>
    ''' <param name="InputReport"></param>
    ''' <returns>TRUE if the request was sent successfully, FALSE if there was an error. You can use GetLastWin32Error to get more detailed error information.</returns>
    Private Function DoReadInputReport(ByRef InputReport As HIDInputReport) As Boolean
        Return HidD_GetInputReport(_Stream.SafeFileHandle.DangerousGetHandle, InputReport.GetBuffer, InputReport.ReportLength)
    End Function

    ''' <summary>
    ''' Performs GetFeatureReport for the overloaded ReadFeatureReport methods.
    ''' </summary>
    ''' <param name="FeatureReport"></param>
    ''' <returns>TRUE if the request was sent successfully, FALSE if there was an error. You can use GetLastWin32Error to get more detailed error information.</returns>
    Private Function DoReadFeatureReport(ByRef FeatureReport As HIDFeatureReport) As Boolean
        Return HidD_GetFeature(_Stream.SafeFileHandle.DangerousGetHandle, FeatureReport.GetBuffer, FeatureReport.ReportLength)
    End Function

#End Region

#Region "Async Support"

    Private Sub BeginAsyncRead()
        If _Stream.CanRead And _Stream.IsAsync Then
            '_AsyncTask = Task(Of Integer).Factory.FromAsync(AddressOf _Stream.BeginRead, AddressOf _Stream.EndRead, _AsyncBuffer, 0, _AsyncBuffer.Length, _AsyncBuffer, TaskCreationOptions.None)
            '_AsyncTask.ContinueWith(AddressOf ContinueReading)

            _AsyncResult = _Stream.BeginRead(_AsyncBuffer, 0, _AsyncBuffer.Length, New AsyncCallback(AddressOf OnReportReceived), _AsyncBuffer)
        End If
    End Sub

    Private Sub ContinueReading(t As Task)
        Dim hir As New HIDInputReport(_AsyncBuffer)
        Dim args As New HIDReportReceivedEventArgs(hir)

        RaiseEvent ReportReceived(Me, args)

        If _AsyncReading Then
            _AsyncTask.ContinueWith(AddressOf ContinueReading)
        End If
    End Sub

    Private Sub OnReportReceived(ByVal ar As IAsyncResult)
        'Dim InBuffer As Byte() = CType(ar.AsyncState, Byte())
        Dim hir As New HIDInputReport(_AsyncBuffer)
        Dim args As New HIDReportReceivedEventArgs(hir)

        _Stream.EndRead(ar)

        RaiseEvent ReportReceived(Me, args)

        If _AsyncReading Then
            BeginAsyncRead()
        End If
    End Sub

#End Region

#Region "IEquatable Support"

    Public Overloads Function Equals(other As HIDDevice) As Boolean Implements IEquatable(Of HIDDevice).Equals
        Return (other.DevicePath = _Stream.Name)
    End Function

    Public Overloads Overrides Function Equals(obj As Object) As Boolean
        If Not (TypeOf obj Is HIDDevice) Then
            Return False
        Else
            Return Me.Equals(DirectCast(obj, HIDDevice))
        End If
    End Function

#End Region

#Region "Cleanup / Disposal"

    Protected Overloads Overrides Sub Finalize()
        FreeUnmanagedResources()

        MyBase.Finalize()
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        FreeUnmanagedResources()

        GC.SuppressFinalize(Me)
    End Sub

    Private Sub FreeUnmanagedResources()
        Try
            _Capabilities._Parent = Nothing
            _Capabilities = Nothing

            If _PreparsedData <> IntPtr.Zero Then
                HidD_FreePreparsedData(_PreparsedData)
            End If

            _Stream.Close()
            _Stream.Dispose()
        Catch ex As Exception
            MsgBox("HIDDevice.Free error: " & ex.ToString)
        End Try
    End Sub

#End Region

End Class
